
import {τ} from "../util/consts.js";
import {clamp, rescale, rint} from "../util/math.js";
// import * as d3 from "../lib/d3.js";
import * as d3 from "d3";

import LOG_FRAG from "./log.frag";
import LINEAR_FRAG from "./linear.frag";
import PALETTE_FRAG from "./palette.frag";

function normalizeColor(format) {
    if (Array.isArray(format)) {
        const [r, g, b, a] = format;
        return d3.rgb(r, g, b, a);
    }
    return d3.color(format);
}

function colorInterpolator(start, end) {
    const [r, g, b] = start;
    const Δr = end[0] - r, Δg = end[1] - g, Δb = end[2] - b;
    return t => {
        return [
            Math.floor(r + t * Δr),
            Math.floor(g + t * Δg),
            Math.floor(b + t * Δb),
        ].map(e => clamp(e, 0, 255));
    };
}

function interpolateLabBasis(colors, range) {
    const l = [], a = [], b = [];
    for (let i = 0; i < colors.length; i++) {
        const color = d3.lab(colors[i]);
        l[i] = color.l;
        a[i] = color.a;
        b[i] = color.b;
    }
    const lBasis = d3.interpolateBasis(l);
    const aBasis = d3.interpolateBasis(a);
    const bBasis = d3.interpolateBasis(b);
    const result = t => {
        return d3.lab(lBasis(t), aBasis(t), bBasis(t)) + "";
    };
    result.domain = () => range;
    result.mode = () => "lab";
    return result;
}

function instantiate(mode, a, b) {
    switch (mode) {
        case "rgb": return d3.interpolateRgb(a, b);
        case "lab": return d3.interpolateLab(a, b);
        case "hcl": return d3.interpolateHcl(a, b);
        case "hsl": return d3.interpolateHsl(a, b);
        default: throw new Error(`unsupported color space: ${mode}`);
    }
}

function newStopInterpolator(stopA, stopB) {
    const interpolator = instantiate(stopA.mode, normalizeColor(stopA.color), normalizeColor(stopB.color));
    // UNDONE HACK: stash some additional info on the interpolator function.
    interpolator.mode = () => stopA.mode;
    interpolator.domain = () => [stopA.p, stopB.p];
    return interpolator;
}

export function newInterpolator(scheme, mode, domain) {
    const interpolator = t => scheme(t);
    interpolator.mode = () => mode;
    interpolator.domain = () => domain;
    return interpolator;
}

/**
 * Produces a color style in a rainbow-like trefoil color space. Not quite HSV, but produces a nice
 * spectrum. See http://krazydad.com/tutorials/makecolors.php.
 *
 * @param hue the hue rotation in the range [0, 1]
 * @returns {Array} [r, g, b]
 */
export function sinebowColor(hue) {
    // Map hue [0, 1] to radians [0, 5/6τ]. Don't allow a full rotation because that keeps hue == 0 and
    // hue == 1 from mapping to the same color.
    let rad = hue * τ * 5/6;
    rad *= 0.75;  // increase frequency to 2/3 cycle per rad

    const s = Math.sin(rad);
    const c = Math.cos(rad);
    const r = Math.floor(Math.max(0, -c) * 255);
    const g = Math.floor(Math.max(s, 0) * 255);
    const b = Math.floor(Math.max(c, 0, -s) * 255);
    return [r, g, b];
}

const BOUNDARY = 0.45;
const fadeToWhite = colorInterpolator(sinebowColor(1), [255, 255, 255]);

/**
 * Interpolates a sinebow color where 0 <= t <= j, then fades to white where j < t <= 1.
 *
 * @param t number in the range [0, 1] inclusive
 * @returns {Array} [r, g, b]
 */
export function extendedSinebowColor(t) {
    return t <= BOUNDARY ?
        sinebowColor(t / BOUNDARY) :
        fadeToWhite((t - BOUNDARY) / (1 - BOUNDARY));
}

/**
 * Creates a color scale composed of the specified segments. Segments is an array of two-element arrays of the
 * form [value, color], where value is the point along the scale and color is the [r, g, b] color at that point.
 * For example, the following creates a scale that smoothly transitions from red to green to blue along the
 * points 0.5, 1.0, and 3.5:
 *
 *     [ [ 0.5, [255,   0,   0] ],
 *       [ 1.0, [  0, 255,   0] ],
 *       [ 3.5, [  0,   0, 255] ] ]
 *
 * @param segments array of color segments
 * @returns {Function} a function(v) that returns the color [r, g, b] for the given value.
 */
export function segmentedColorScale(segments) {
    const points = [], interpolators = [], ranges = [];
    for (let i = 0; i < segments.length - 1; i++) {
        points.push(segments[i+1][0]);
        interpolators.push(colorInterpolator(segments[i][1], segments[i+1][1]));
        ranges.push([segments[i][0], segments[i+1][0]]);
    }

    return v => {
        let i;
        for (i = 0; i < points.length - 1; i++) {
            if (v <= points[i]) {
                break;
            }
        }
        const t = rescale(v, ...ranges[i], 0, 1);
        return interpolators[i](t);
    };
}

/**
 * Converts an array of N color stops into N-1 D3 interpolators:
 *
 *    {color: "black", mode: "hcl", p: 10},
 *    {color: "grey",  mode: "lab", p: 20},
 *    {color: "white",              p: 30}
 *
 * Result:
 *    scale 0 [black, grey] over domain [10, 20] in lch color space
 *    scale 1 [grey, white] over domain [20, 30] in lab color space
 *
 * @param {Array} stops an array of colors stops to convert into scales.
 * @returns {Array} an array of D3 interpolator functions.
 */
export function interpolatorsFrom(stops) {
    const interpolators = [];
    for (let i = 0; i < stops.length - 1; i++) {
        interpolators.push(newStopInterpolator(stops[i], stops[i + 1]));
    }
    return interpolators;
}

/**
 * Given a range [a, b] and two adjoining color scales L (i) and R (i+1) sharing domain point p, insert a new
 * basis spline interpolated scale M over the points [a, p, b].
 *
 * @param {Array} interpolators
 * @param {Number} i
 * @param {Array} range
 * @returns {Array}
 */
export function smooth(interpolators, i, range) {
    //         p                  p
    //        / \              M _-_
    //       /   \      =>      /   \
    //      /     \            a     b
    //   L /       \ R      K /       \ S
    //    Lx       Ry        Lx       Ry

    const L = interpolators[i], R = interpolators[i+1];
    const a = rescale(range[0], ...L.domain(), 0, 1);
    const b = rescale(range[1], ...R.domain(), 0, 1);

    const K = newStopInterpolator(
        {color: d3.color(L(0)), mode: L.mode(), p: L.domain()[0]},
        {color: d3.color(L(a)), mode: L.mode(), p: range[0]}
    );
    const M = interpolateLabBasis([L(a), L(1), R(b)], range);
    const S = newStopInterpolator(
        {color: d3.color(R(b)), mode: R.mode(), p: range[1]},
        {color: d3.color(R(1)), mode: R.mode(), p: R.domain()[1]}
    );

    return [].concat(interpolators.slice(0, i), [K, M, S], interpolators.slice(i+2));
}

/**
 * Use array A of length n to define a linear scale over domain [x, y] such that [x, y] is mapped onto indices
 * [0, n-1]. The range [a, b] is then mapped to indices [i, j] using this scale, and the elements A[i] to A[j] are
 * filled with the results of f(v) where v iterates over [a, b].
 *
 * @param {Uint8Array} array the destination array to fill as rgba quadlets: [r0, g0, b0, a0, ...]
 * @param {Number[]} domain the values [x, y], inclusive.
 * @param {Number[]} range the values [a, b], inclusive.
 * @param {Function} ƒcolor the value function f(v) that returns [r, g, b] or [r, g, b, opacity] for v.
 */
export function fillRange(array, domain, range, ƒcolor) {

    //    |-----------domain------------|
    //    |        |---range---|        |
    //    x        a           b        y
    //    0        p           q       n-1
    // A [0, ..., f(a), ..., f(b), ..., 0] n

    const [x, y] = domain;
    const [a, b] = range;
    const n = Math.floor(array.length / 4);
    const Δ = (y - x) / (n - 1);
    const p = rint((a - x) / Δ);

    for (let i = Math.max(p, 0); i < n; i++) {
        const value = a + (i - p) * Δ;
        if (value > b) {
            break;
        }
        const c = ƒcolor(value);
        const j = i * 4;
        array[j  ] = c[0];
        array[j+1] = c[1];
        array[j+2] = c[2];
        array[j+3] = c.length > 3 ? clamp(rescale(c[3], 0, 1, 0, 255), 0, 255) : 255;
    }
}

/**
 * Convert a set of D3 interpolators into an accessor function over a computed array of rgba colors.
 *
 * @param {Number[]} bounds the values [x, y], inclusive.
 * @param {Array} interpolators the set of interpolator functions.
 * @param {Number} resolution the number of elements of the computed color array.
 * @returns {Uint8Array} sequence of rgba quadlets: [r0, g0, b0, a0, r1, g1, b1, a1, ...]
 */
export function quantize(bounds, interpolators, resolution) {
    const array = new Uint8Array(resolution * 4);
    interpolators.forEach(interpolator => {
        const domain = interpolator.domain();
        // TODO: BUG: some combinations of bounds and resolution leave the end of the array unchanged as [...,0,0,0,0].
        //      For example
        //          bounds: [380, 509], domain: [488, 509], resolution: 1000
        //      where as this works:
        //          bounds: [380, 510], domain: [488, 510], resolution: 1000
        fillRange(array, bounds, domain, v => {
            const c = d3.color(interpolator(rescale(v, domain[0], domain[1], 0, 1)));
            return [c.r, c.g, c.b, c.opacity];
        });
    });
    return array;
}

export function buildScaleFromInterpolators(bounds, interpolators, resolution) {
    const colors = quantize(bounds, interpolators, resolution);
    return buildScale(bounds, colors);
}

export function buildScaleFromSegments(bounds, segments, resolution) {
    const gradient = segmentedColorScale(segments);
    const array = new Uint8Array(resolution * 4);
    fillRange(array, bounds, bounds, gradient);
    return buildScale(bounds, array);
}

/**
 * @param {Number[]} bounds [low, high] values. Assumes bounds are _center_ aligned color stops.
 * @param {Uint8Array} colors sequence of rgba quadlets: [r0, g0, b0, a0, r1, g1, b1, a1, ...]
 * @param {Function} [ƒmap] the scale function, like Math.log (default is linear).
 * @param {Function} [ƒinv] the inverse scale function, like Math.exp (default is linear).
 * @returns {Scale}
 */
export function buildScale(bounds, colors, ƒmap = (v=>v), ƒinv = (v=>v)) {

    /*

    Two types of scale representations:

    0        10        20        30        40        50   <= Split range [0, 50] (inclusive) over 5 buckets.
    |         |         |         |         |         |      Stops are aligned with _edges_ of buckets.
    +---------+---------+---------+---------+---------+
    | 0 black | 1 blue  | 2 green | 3  red  | 4 white |   array.length == n == 5
    +---------+---------+---------+---------+---------+
         |         |         |         |         |
         5        15        25        35        45        <= Color stops define _centers_, where distance between
                                                             stops is bucket size. Range here is [5, 45].

    These two scales are equivalent but use different formulas to map value -> index for array access, and
    value -> texcoord on range [0, 1].

    For edge aligned stops:
        texcoord s = (value - edgeLow) / (edgeHigh - edgeLow)
        index    i = s * n - 0.5

    For center aligned stops:
        index    i = (value - centerLow) / (centerHigh - centerLow) * (n - 1)
        texcoord s = (i + 0.5) / n

    To convert between the two different scale types, expand/contract the bounds based on bucket count:
        [centerLow, centerHigh] == [edgeLow + ε, edgeHigh - ε]  where ε = (edgeHigh - edgeLow) / 2n
        [edgeLow, edgeHigh] == [centerLow - ε, centerHigh + ε]  where ε = (centerHigh - centerLow) / 2(n-1)

     */

    const lo = ƒmap(bounds[0]), hi = ƒmap(bounds[1]);
    const iMax = colors.length / 4 - 1, scale = iMax / (hi - lo);
    const hash = {};
    const ε = (hi - lo) / (2 * iMax);
    const edgeLo = lo - ε, edgeHi = hi + ε, edgeRange = [edgeLo, edgeHi - edgeLo];

    return new class Scale {

        /**
         * @returns {Uint8Array} [r0, g0, b0, a0, r1, g1, b1, a1, ...]
         */
        get colors() {
            return colors;
        }

        /**
         * @returns {Number[]} [low, high] bounds of this scale.
         */
        get bounds() {
            return bounds;
        }

        /**
         * @param {Number} value the scale value
         * @returns {Number} the rgba quadlet index; multiply by 4 for the true index in the colors array.
         */
        indexOf(value) {
            const i = Math.round((ƒmap(value) - lo) * scale);
            return clamp(i, 0, iMax);
        }

        /**
         * @param index the rgba quadlet index
         * @returns {Number} the associated scale value
         */
        valueFor(index) {
            return ƒinv(index / scale + lo);
        }

        /**
         * @param t the unit value [0, 1] percentage through the bounds of the scale
         * @returns {Number} the associated scale value
         */
        valueInRange(t) {
            const i = Math.round(rescale(t, 0, 1, 0, iMax));
            return this.valueFor(clamp(i, 0, iMax));
        }

        /**
         * @param {Number} value the scale value
         * @returns {Number[]} rgba quadlet for the specified value
         */
        rgba(value) {
            const j = this.indexOf(value) * 4;
            return [colors[j], colors[j+1], colors[j+2], colors[j+3]];
        }

        /**
         * @param {GLUStick} glu
         */
        webgl(glu) {
            const gl = glu.context;
            return {
                shaderSource: [
                    ƒmap === Math.log ? LOG_FRAG : LINEAR_FRAG,
                    PALETTE_FRAG,
                ],
                textures: {
                    color_scale: {
                        format: gl.RGBA,
                        type: gl.UNSIGNED_BYTE,
                        width: colors.length / 4,
                        height: 1,
                        pixels: colors,
                        hash: hash,
                    },
                },
                uniforms: {
                    u_Range: edgeRange,
                    u_Palette: "color_scale",
                    u_Alpha: 1.0,
                },
            }
        }

    }();

}
